import { GraphStyle } from "@/utils/styles";

import editorStyle from "../util/defaultStyle";
import { G6 } from "@/g6/g6";

const deepMix = G6.Util.deepMix;

const uniqBy = (arr, key) => {
  const result = [];
  arr.forEach((i) => {
    if (!result.find((r) => r[key] === i[key])) result.push(i);
  });
  return result;
};

export function registerEdge(G6) {
  G6.registerEdge(
    "flow-polyline-round",
    {
      options: {
        style: {
          ...GraphStyle.default.defaultEdge.style,
        },
        stateStyles: {
          ...GraphStyle.default.edgeStateStyles,
        },
      },
      setState(name, value, item) {
        const shape = item.get("keyShape");
        if (!shape) {
          return;
        }
        const itemStateStyle = item.getStateStyle(name);
        const stateStyle = this.getStateStyle(name, value, item);
        const styles = deepMix({}, stateStyle, itemStateStyle);
        if (value) {
          // 如果设置状态,在原本状态上叠加绘图属性
          shape.attr(styles);
        } else {
          // 取消状态时重置所有状态，依次叠加仍有的状态
          const style = item.getCurrentStatesStyle();
          // 如果默认状态下没有设置attr，在某状态下设置了，需要重置到没有设置的状态
          G6.Util.each(styles, (val, attr) => {
            if (!style[attr]) {
              style[attr] = null;
            }
          });
          shape.attr(style);
        }
      },
      drawShape(cfg, group) {
        this.group = group;
        const shapeStyle = this.getShapeStyle(cfg);
        const shape = group.addShape("path", {
          className: "edge-shape",
          attrs: shapeStyle,
        });
        return shape;
      },
      drawLabel(cfg, group) {
        const labelCfg = cfg.labelCfg || {};
        const labelStyle = this.getLabelStyle(cfg, labelCfg, group);
        const label = group.addShape("text", {
          attrs: labelStyle,
        });
        const labelBBox = label.getBBox();
        group.addShape("rect", {
          className: "edge-labelRect",
          attrs: {
            x: labelBBox.x - editorStyle.edgeLabelRectPadding / 2,
            y: labelBBox.y - editorStyle.edgeLabelRectPadding / 2,
            width: labelBBox.width + editorStyle.edgeLabelRectPadding,
            height: labelBBox.height + editorStyle.edgeLabelRectPadding,
            fill: "#fff",
            stroke: "#fff",
          },
        });
        group.toBack();
        label.toFront();
        return label;
      },
      afterUpdate(cfg, item) {
        const label = item.getContainer().findByClassName("edge-label");
        const labelRect = item.getContainer().findByClassName("edge-labelRect");
        if (label) {
          const labelBBox = label.getBBox();
          labelRect.attr({
            x: labelBBox.x - editorStyle.edgeLabelRectPadding / 2,
            y: labelBBox.y - editorStyle.edgeLabelRectPadding / 2,
            width: labelBBox.width + editorStyle.edgeLabelRectPadding,
            height: labelBBox.height + editorStyle.edgeLabelRectPadding,
          });
        }
      },
      getShapeStyle(cfg) {
        cfg = this.getPathPoints(cfg);
        const startPoint = cfg.startPoint;
        const endPoint = cfg.endPoint;
        const controlPoints = this.getControlPoints(cfg);
        let points = [startPoint];
        if (controlPoints) {
          points = points.concat(controlPoints);
        }
        points.push(endPoint);
        const path = this.getPath(points);
        let style = this.options.style;
        if (cfg.reverse) style = { ...style, lineDash: [1, 3] };
        else style = { ...style, lineDash: null };
        return {
          path,
          ...style,
          endArrow: {
            path: "M 0,0 L -10,-4 S -8 0,-10 4 Z",
          },
        };
      },
      getPath(points) {
        const path = [];
        for (let i = 0; i < points.length; i++) {
          const point = points[i];
          if (i === 0) {
            path.push(["M", point.x, point.y]);
          } else if (i === points.length - 1) {
            path.push(["L", point.x, point.y]);
          } else {
            const prevPoint = points[i - 1];
            let nextPoint = points[i + 1];
            let cornerLen = 5;
            if (Math.abs(point.y - prevPoint.y) > cornerLen || Math.abs(point.x - prevPoint.x) > cornerLen) {
              if (prevPoint.x === point.x) {
                path.push(["L", point.x, point.y > prevPoint.y ? point.y - cornerLen : point.y + cornerLen]);
              } else if (prevPoint.y === point.y) {
                path.push(["L", point.x > prevPoint.x ? point.x - cornerLen : point.x + cornerLen, point.y]);
              }
            }
            const yLen = Math.abs(point.y - nextPoint.y);
            const xLen = Math.abs(point.x - nextPoint.x);
            if (yLen > 0 && yLen < cornerLen) {
              cornerLen = yLen;
            } else if (xLen > 0 && xLen < cornerLen) {
              cornerLen = xLen;
            }
            if (prevPoint.x !== nextPoint.x && nextPoint.x === point.x) {
              path.push([
                "Q",
                point.x,
                point.y,
                point.x,
                point.y > nextPoint.y ? point.y - cornerLen : point.y + cornerLen,
              ]);
            } else if (prevPoint.y !== nextPoint.y && nextPoint.y === point.y) {
              path.push([
                "Q",
                point.x,
                point.y,
                point.x > nextPoint.x ? point.x - cornerLen : point.x + cornerLen,
                point.y,
              ]);
            }
          }
        }
        return path;
      },
      getControlPoints(cfg) {
        if (!cfg.sourceNode) {
          return cfg.controlPoints;
        }
        return this.polylineFinding(cfg.sourceNode, cfg.targetNode, cfg.startPoint, cfg.endPoint, 15);
      },
      getExpandedBBox(bbox, offset) {
        return 0 === bbox.width && 0 === bbox.height
          ? bbox
          : {
              centerX: bbox.centerX,
              centerY: bbox.centerY,
              minX: bbox.minX - offset,
              minY: bbox.minY - offset,
              maxX: bbox.maxX + offset,
              maxY: bbox.maxY + offset,
              height: bbox.height + 2 * offset,
              width: bbox.width + 2 * offset,
            };
      },
      getExpandedPort(bbox, point) {
        return Math.abs(point.x - bbox.centerX) / bbox.width > Math.abs(point.y - bbox.centerY) / bbox.height
          ? { x: point.x > bbox.centerX ? bbox.maxX : bbox.minX, y: point.y }
          : { x: point.x, y: point.y > bbox.centerY ? bbox.maxY : bbox.minY };
      },
      combineBBoxes(sBBox, tBBox) {
        const minX = Math.min(sBBox.minX, tBBox.minX),
          minY = Math.min(sBBox.minY, tBBox.minY),
          maxX = Math.max(sBBox.maxX, tBBox.maxX),
          maxY = Math.max(sBBox.maxY, tBBox.maxY);
        return {
          centerX: (minX + maxX) / 2,
          centerY: (minY + maxY) / 2,
          minX: minX,
          minY: minY,
          maxX: maxX,
          maxY: maxY,
          height: maxY - minY,
          width: maxX - minX,
        };
      },
      getBBoxFromVertexes(sPoint, tPoint) {
        const minX = Math.min(sPoint.x, tPoint.x),
          maxX = Math.max(sPoint.x, tPoint.x),
          minY = Math.min(sPoint.y, tPoint.y),
          maxY = Math.max(sPoint.y, tPoint.y);
        return {
          centerX: (minX + maxX) / 2,
          centerY: (minY + maxY) / 2,
          maxX: maxX,
          maxY: maxY,
          minX: minX,
          minY: minY,
          height: maxY - minY,
          width: maxX - minX,
        };
      },
      vertexOfBBox(bbox) {
        return [
          { x: bbox.minX, y: bbox.minY },
          { x: bbox.maxX, y: bbox.minY },
          { x: bbox.maxX, y: bbox.maxY },
          { x: bbox.minX, y: bbox.maxY },
        ];
      },
      crossPointsByLineAndBBox(bbox, centerPoint) {
        let crossPoints = [];
        if (!(centerPoint.x < bbox.minX || centerPoint.x > bbox.maxX))
          crossPoints = crossPoints.concat([
            { x: centerPoint.x, y: bbox.minY },
            { x: centerPoint.x, y: bbox.maxY },
          ]);
        if (!(centerPoint.y < bbox.minY || centerPoint.y > bbox.maxY))
          crossPoints = crossPoints.concat([
            { x: bbox.minX, y: centerPoint.y },
            { x: bbox.maxX, y: centerPoint.y },
          ]);
        return crossPoints;
      },
      getConnectablePoints(sBBox, tBBox, sPoint, tPoint) {
        const lineBBox = this.getBBoxFromVertexes(sPoint, tPoint);
        const outerBBox = this.combineBBoxes(sBBox, tBBox);
        const sLineBBox = this.combineBBoxes(sBBox, lineBBox);
        const tLineBBox = this.combineBBoxes(tBBox, lineBBox);
        let points = [];
        points = points.concat(
          this.vertexOfBBox(sLineBBox),
          this.vertexOfBBox(tLineBBox),
          this.vertexOfBBox(outerBBox)
        );
        const centerPoint = { x: outerBBox.centerX, y: outerBBox.centerY };
        [outerBBox, sLineBBox, tLineBBox, lineBBox].forEach((bbox) => {
          points = points.concat(this.crossPointsByLineAndBBox(bbox, centerPoint));
        });
        points.push({ x: sPoint.x, y: tPoint.y });
        points.push({ x: tPoint.x, y: sPoint.y });
        return points;
      },
      filterConnectablePoints(points, bbox) {
        return points.filter(
          (point) => point.x <= bbox.minX || point.x >= bbox.maxX || point.y <= bbox.minY || point.y >= bbox.maxY
        );
      },
      AStar(points, sPoint, tPoint, sBBox, tBBox) {
        const openList = [sPoint];
        const closeList = [];
        points = uniqBy(this.fillId(points), "id");
        points.push(tPoint);
        let endPoint;
        while (openList.length > 0) {
          let minCostPoint;
          openList.forEach((p, i) => {
            if (!p.parent) p.f = 0;
            if (!minCostPoint) minCostPoint = p;
            if (p.f < minCostPoint.f) minCostPoint = p;
          });
          if (minCostPoint.x === tPoint.x && minCostPoint.y === tPoint.y) {
            endPoint = minCostPoint;
            break;
          }
          openList.splice(
            openList.findIndex((o) => o.x === minCostPoint.x && o.y === minCostPoint.y),
            1
          );
          closeList.push(minCostPoint);
          const neighbor = points.filter(
            (p) =>
              (p.x === minCostPoint.x || p.y === minCostPoint.y) &&
              !(p.x === minCostPoint.x && p.y === minCostPoint.y) &&
              !this.crossBBox([sBBox, tBBox], minCostPoint, p)
          );
          neighbor.forEach((p) => {
            const inOpen = openList.find((o) => o.x === p.x && o.y === p.y);
            const currentG = this.getCost(p, minCostPoint);
            if (closeList.find((o) => o.x === p.x && o.y === p.y)) {
            } else if (inOpen) {
              if (p.g > currentG) {
                p.parent = minCostPoint;
                p.g = currentG;
                p.f = p.g + p.h;
              }
            } else {
              p.parent = minCostPoint;
              p.g = currentG;
              let h = this.getCost(p, tPoint);
              if (this.crossBBox([tBBox], p, tPoint)) {
                h += tBBox.width / 2 + tBBox.height / 2; //如果穿过bbox则增加该点的预估代价为bbox周长的一半
              }
              p.h = h;
              p.f = p.g + p.h;
              openList.push(p);
            }
          });
        }
        if (endPoint) {
          const result = [];
          result.push({ x: endPoint.x, y: endPoint.y });
          while (endPoint.parent) {
            endPoint = endPoint.parent;
            result.push({ x: endPoint.x, y: endPoint.y });
          }
          return result.reverse();
        }
        return [];
      },
      crossBBox(bboxes, p1, p2) {
        for (let i = 0; i < bboxes.length; i++) {
          const bbox = bboxes[i];
          if (p1.x === p2.x && bbox.minX < p1.x && bbox.maxX > p1.x) {
            if ((p1.y < bbox.maxY && p2.y >= bbox.maxY) || (p2.y < bbox.maxY && p1.y >= bbox.maxY)) return true;
          } else if (p1.y === p2.y && bbox.minY < p1.y && bbox.maxY > p1.y) {
            if ((p1.x < bbox.maxX && p2.x >= bbox.maxX) || (p2.x < bbox.maxX && p1.x >= bbox.maxX)) return true;
          }
        }
        return false;
      },
      getCost(p1, p2) {
        return Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y);
      },
      getPointBBox(t) {
        return { centerX: t.x, centerY: t.y, minX: t.x, minY: t.y, maxX: t.x, maxY: t.y, height: 0, width: 0 };
      },
      fillId(points) {
        points.forEach((p) => {
          p.id = p.x + "-" + p.y;
        });
        return points;
      },
      polylineFinding(sNode, tNode, sPort, tPort, offset) {
        const sourceBBox = sNode && sNode.getBBox() ? sNode.getBBox() : this.getPointBBox(sPort);
        const targetBBox = tNode && tNode.getBBox() ? tNode.getBBox() : this.getPointBBox(tPort);
        const sBBox = this.getExpandedBBox(sourceBBox, offset);
        const tBBox = this.getExpandedBBox(targetBBox, offset);
        const sPoint = this.getExpandedPort(sBBox, sPort);
        const tPoint = this.getExpandedPort(tBBox, tPort);
        let points = this.getConnectablePoints(sBBox, tBBox, sPoint, tPoint);
        points = this.filterConnectablePoints(points, sBBox);
        points = this.filterConnectablePoints(points, tBBox);
        const polylinePoints = this.AStar(points, sPoint, tPoint, sBBox, tBBox);
        return polylinePoints;
      },
    },
    "polyline"
  );
}
